import csv
import sys
import os
import re
import logging
from pprint import pprint
from StringIO import StringIO
from optparse import make_option

from django.utils.encoding import smart_str
from django.core.management.base import BaseCommand
from reviewboard.reviews.models import Comment, ReviewRequest
from reviewboard.diffviewer.models import FileDiff

# TODO: Count ship it


DATETIME_FORMAT     = '%Y-%m-%d %H:%M'
RE_NORMALIZE_SPACES = re.compile('\s{2,}')
LOGGING_FORMAT      = '%(asctime)-15s %(levelname)-8s: %(message)s'


def one_line(text, length=45):
    """
    Simpel function to produce one-line comments.
    """
    text = text.strip().replace('\r', '').replace('\n', ' ')
    text = RE_NORMALIZE_SPACES.sub(' ', text)
    if length and len(text) > length:
        return text[:length-4]+'...'
    return text


def write_to_csv(file_path, item_order_list, item_list):
    """
    Writes a list of dicts to a CSV file using a order list
    to define the order of items to write.
    """
    logging.info('Starting to write CSV file: "%s"' % file_path)
    output = StringIO()
    writer = csv.writer(output, dialect=csv.excel)
    writer.writerow(item_order_list)
    for item in item_list:
        L = []
        for key in item_order_list:
            try:
                L.append(smart_str(item[key]))
            except KeyError:
                L.append(None)
        writer.writerow(L)
    f = open(file_path, 'wb')
    f.write(output.getvalue())
    f.close()
    output.close()
    logging.info('Done writing CSV file: "%s"' % file_path)


def get_unified_diff_loc(unified_diff):
    """
    Calculates the LOC (lines of code) score for a given unified diff.

    Motorola states that LOC is counted in the following manner:
    - deleted line -> 0.5 line
    - added line   -> 1.0 line
    - edited line  -> 1.0 line
    
    For more information on unified diffs, see http://en.wikipedia.org/wiki/Diff
    """
    loc = 0.0
    block_del = False
    cnt_del = 0
    cnt_add = 0
    cnt_cng = 0
    for line in unified_diff.replace('\r\n', '\n').split('\n'):
        if line and not line.startswith('---') and not line.startswith('+++'):
            if '-' == line[0]:
                block_del = True
                cnt_del += 1
            elif '+' == line[0]:
                if block_del and cnt_del > 0:
                    cnt_del -= 1
                    cnt_cng += 1
                else:
                    cnt_add += 1
            elif ' ' == line[0] or '@' == line[0] or '=' == line[0]:
                loc += float(cnt_cng + cnt_add) + 0.5 * cnt_del
                cnt_cng = cnt_add = cnt_del = 0
                block_del = False
            elif line.startswith('Index:') or '\\' == line[0]:
                pass
            else:
                raise RuntimeError('hej')
    loc += float(cnt_cng + cnt_add) + 0.5 * cnt_del
    return loc


def get_loc_from_file_diffs(file_diff_ids):
    if not file_diff_ids:
        raise RuntimeError('No diff IDs in list')
    LOC = 0
    for file_diff in FileDiff.objects.filter(id__in=file_diff_ids):
        LOC += get_unified_diff_loc(file_diff.diff)
    return LOC


def type_counts_from_comments(comment_ids):
    D = {}
    for comment in Comment.objects.filter(id__in=comment_ids).order_by('-timestamp'):
        if not comment.first_line in D:
            D[comment.first_line] = comment.get_type_display()
        if comment.num_lines != 1:
            logging.warning('Correct handling of multi-line comments is not implemented yet [Comment:<%d> %s]' % (comment.id, one_line(comment.text)))
    R = {}
    for value in D.itervalues():
        try:
            R[value] += 1
        except KeyError:
            R[value] = 1
    return R


def get_review_request_metrics(review_request):
    """
    Calculates metrics for a given review request and returns a tubel containing
    the overall metrics as a dict and list containing one dict with metrics for
    each comment.
    """
    D = {'id'          : int(review_request.id),
         'summary'     : one_line(review_request.summary, None),
         'time_added'  : review_request.time_added.strftime(DATETIME_FORMAT),
         'last_updated': review_request.last_updated.strftime(DATETIME_FORMAT),
         'bugs'        : review_request.bugs_closed,
         }
    for type_info in Comment.TYPES:
        D[type_info[1]] = 0
    L_reviewers     = []
    L_file_diffs    = []
    L_comment_ids   = []
    L_comments_info = []
    logging.info('SUMMARY: %s' % review_request.summary)
    for file_diff in FileDiff.objects.filter(diffset__history__review_request=review_request).distinct():
        logging.info('FILE DIFF: %s' % file_diff.dest_file)
        L_file_diffs.append(file_diff.id)
    for review in review_request.reviews.select_related(depth=1).filter(public=True):
        if review.user_id not in L_reviewers:
            L_reviewers.append(review.user_id)
        for comment in review.comments.select_related(depth=1):
            logging.info('COMMENT: %d - %s' % (comment.id, one_line(comment.text)))
            L_comment_ids.append(comment.id)
            L_comments_info.append({'review_request_id': review_request.id,
                                    'timestamp'        : comment.timestamp.strftime(DATETIME_FORMAT),
                                    'username'         : review.user.username,
                                    'type'             : comment.get_type_display(),
                                    'phase_originated' : comment.get_phase_originated_display(),
                                    'first_line'       : comment.first_line,
                                    'num_lines'        : comment.num_lines,
                                    'file'             : comment.filediff.dest_file,
                                    'text'             : one_line(comment.text, None),
                                    })
    D.update(type_counts_from_comments(L_comment_ids))
    D['reviewer_count'] = len(L_reviewers)
    D['num_files']      = len(L_file_diffs)
    D['num_comments']   = len(L_comment_ids)
    D['LOC']            = get_loc_from_file_diffs(L_file_diffs)
    return (D, L_comments_info)


class Command(BaseCommand):
    option_list = BaseCommand.option_list + (
        make_option('--debug', default=False, dest='debug', action='store_true',
            help='Output debug information'),
        make_option('--log-file', default=None, dest='log_file', metavar='FILE',
            help='File to store logging information to'),
            )
    
    def handle(self, *args, **options):
        # LOGGING ======================================================================
        logging_level = logging.WARNING
        if options.get('debug'):
            logging_level = logging.DEBUG
        logging.basicConfig(level=logging_level,
                            format=LOGGING_FORMAT)
        log_file = options.get('log_file')
        if log_file:
            log_file = os.path.abspath(log_file)
            if os.path.exists(log_file):
                logging.error('Log file "%s" already exist, please delete it' % log_file)
                sys.exit()
            hdlr = logging.FileHandler(log_file)
            hdlr.setFormatter(logging.Formatter(LOGGING_FORMAT))
            logger = logging.getLogger()
            logger.addHandler(hdlr)
        # LOGGING DONE =================================================================
        
        # OPTIONS ======================================================================
        if 1 != len(args):
            sys.exit('ERROR: Provide at least one argument to store the report in')
        base_path = os.path.abspath(args[0])
        if not (base_path.lower().endswith('.csv') or base_path.lower().endswith('.xls')):
            sys.exit('ERROR: Script only supports outputting CSV and XLS files, please put a ".csv" or ".xls" at the end')
        file_type = base_path.rsplit('.', 1)[1].lower()
        base_path = base_path.rsplit('.', 1)[0]
        path_general  = '%s.%s' % (base_path, file_type)
        if 'csv' == file_type:
            path_comments = '%s_comments.%s' % (base_path, file_type)
            if os.path.exists(path_comments):
                sys.exit('ERROR: A file alerady exist at "%s"' % path_comments)
            print 'Comments are written to', path_comments
        elif 'xls' == file_type:
            try:
                from moto.excel import write_to_excel
            except ImportError:
                sys.exit('ERROR: You do not have the moto.excel.ExcelDocument class in your PYTHONPATH and thus Excel export is not an option.')
        if os.path.exists(path_general):
            sys.exit('ERROR: A file alerady exist at "%s"' % path_general)
        print 'General statistics is written to', path_general
        # OPTIONS DONE =================================================================
        
        if 0 != ReviewRequest.objects.exclude(status=ReviewRequest.SUBMITTED).count():
            for review_request in ReviewRequest.objects.exclude(status=ReviewRequest.SUBMITTED):
                logging.warning('Skipping review request <%-14s>: %s' % (review_request.get_status_display().lower(), review_request.summary))
        L_info     = []
        L_comments = []
        print 'Creating review report'
        for review_request in ReviewRequest.objects.filter(status=ReviewRequest.SUBMITTED):
            D_info, L = get_review_request_metrics(review_request)
            L_info.append(D_info)
            L_comments += L
        O_comments = ['review_request_id',
                      'timestamp',
                      'username',
                      'type',
                      'phase_originated',
                      'first_line',
                      'num_lines',
                      'file',
                      'text',
                      ]
        O_general = ['id',
                     'time_added',
                     'last_updated',
                     'LOC',
                     'num_comments',
                     'bugs',
                     'summary',
                     'reviewer_count',
                     'num_files',
                     ] + [S[1] for S in Comment.TYPES]
        if 'csv' == file_type:
            write_to_csv(path_comments, O_comments, L_comments)
            write_to_csv(path_general, O_general, L_info)
        elif 'xls' == file_type:
            from moto.excel import write_to_excel
            write_to_excel(path_general, O_comments, L_comments, 'Comments')
            write_to_excel(path_general, O_general, L_info, 'General')
        else:
            raise NotImplementedError('ERROR: File type "%s" is not supported' % file_type)

